Română

Explorați fundamentele Arborilor Binari de Căutare (BST) și învățați cum să îi implementați eficient în JavaScript. Acest ghid acoperă structura, operațiile și exemple practice pentru dezvoltatorii din întreaga lume.

Arbori Binari de Căutare: Un Ghid Complet de Implementare în JavaScript

Arborii Binari de Căutare (BST) sunt o structură de date fundamentală în informatică, utilizată pe scară largă pentru căutarea, sortarea și recuperarea eficientă a datelor. Structura lor ierarhică permite o complexitate a timpului logaritmică în multe operații, făcându-i un instrument puternic pentru gestionarea seturilor mari de date. Acest ghid oferă o privire de ansamblu cuprinzătoare asupra BST-urilor și demonstrează implementarea lor în JavaScript, adresându-se dezvoltatorilor din întreaga lume.

Înțelegerea Arborilor Binari de Căutare

Ce este un Arbore Binar de Căutare?

Un Arbore Binar de Căutare este o structură de date bazată pe arbori unde fiecare nod are cel mult doi copii, denumiți copilul stâng și copilul drept. Proprietatea cheie a unui BST este că pentru orice nod dat:

Această proprietate asigură că elementele dintr-un BST sunt întotdeauna ordonate, permițând căutarea și recuperarea eficientă.

Concepte Cheie

Implementarea unui Arbore Binar de Căutare în JavaScript

Definirea Clasei Node

Mai întâi, definim o clasă `Node` pentru a reprezenta fiecare nod în BST. Fiecare nod va conține o `key` pentru a stoca datele și pointeri `left` și `right` către copiii săi.


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

Definirea Clasei BinarySearchTree

Apoi, definim clasa `BinarySearchTree`. Această clasă va conține nodul rădăcină și metode pentru inserarea, căutarea, ștergerea și parcurgerea arborelui.


class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  // Metodele vor fi adăugate aici
}

Inserarea

Metoda `insert` adaugă un nod nou cu cheia dată în BST. Procesul de inserare menține proprietatea BST prin plasarea noului nod în poziția corespunzătoare față de nodurile existente.


insert(key) {
  const newNode = new Node(key);

  if (this.root === null) {
    this.root = newNode;
  } else {
    this.insertNode(this.root, newNode);
  }
}

insertNode(node, newNode) {
  if (newNode.key < node.key) {
    if (node.left === null) {
      node.left = newNode;
    } else {
      this.insertNode(node.left, newNode);
    }
  } else {
    if (node.right === null) {
      node.right = newNode;
    } else {
      this.insertNode(node.right, newNode);
    }
  }
}

Exemplu: Inserarea valorilor în BST


const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

Căutarea

Metoda `search` verifică dacă un nod cu cheia dată există în BST. Traversează arborele, comparând cheia cu cheia nodului curent și deplasându-se la subarborele stâng sau drept corespunzător.


search(key) {
  return this.searchNode(this.root, key);
}

searchNode(node, key) {
  if (node === null) {
    return false;
  }

  if (key < node.key) {
    return this.searchNode(node.left, key);
  } else if (key > node.key) {
    return this.searchNode(node.right, key);
  } else {
    return true;
  }
}

Exemplu: Căutarea unei valori în BST


console.log(bst.search(9));  // Ieșire: true
console.log(bst.search(2));  // Ieșire: false

Ștergerea

Metoda `remove` șterge un nod cu cheia dată din BST. Aceasta este cea mai complexă operație, deoarece trebuie să mențină proprietatea BST în timp ce elimină nodul. Există trei cazuri de luat în considerare:


remove(key) {
  this.root = this.removeNode(this.root, key);
}

removeNode(node, key) {
  if (node === null) {
    return null;
  }

  if (key < node.key) {
    node.left = this.removeNode(node.left, key);
    return node;
  } else if (key > node.key) {
    node.right = this.removeNode(node.right, key);
    return node;
  } else {
    // cheia este egală cu cheia nodului

    // cazul 1 - un nod frunză
    if (node.left === null && node.right === null) {
      node = null;
      return node;
    }

    // cazul 2 - nodul are doar 1 copil
    if (node.left === null) {
      node = node.right;
      return node;
    } else if (node.right === null) {
      node = node.left;
      return node;
    }

    // cazul 3 - nodul are 2 copii
    const aux = this.findMinNode(node.right);
    node.key = aux.key;
    node.right = this.removeNode(node.right, aux.key);
    return node;
  }
}

findMinNode(node) {
  let current = node;
  while (current != null && current.left != null) {
    current = current.left;
  }
  return current;
}

Exemplu: Eliminarea unei valori din BST


bst.remove(7);
console.log(bst.search(7)); // Ieșire: false

Parcurgerea Arborelui

Parcurgerea arborelui implică vizitarea fiecărui nod din arbore într-o ordine specifică. Există mai multe metode comune de parcurgere:


inOrderTraverse(callback) {
  this.inOrderTraverseNode(this.root, callback);
}

inOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.inOrderTraverseNode(node.left, callback);
    callback(node.key);
    this.inOrderTraverseNode(node.right, callback);
  }
}

preOrderTraverse(callback) {
  this.preOrderTraverseNode(this.root, callback);
}

preOrderTraverseNode(node, callback) {
  if (node !== null) {
    callback(node.key);
    this.preOrderTraverseNode(node.left, callback);
    this.preOrderTraverseNode(node.right, callback);
  }
}

postOrderTraverse(callback) {
  this.postOrderTraverseNode(this.root, callback);
}

postOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.postOrderTraverseNode(node.left, callback);
    this.postOrderTraverseNode(node.right, callback);
    callback(node.key);
  }
}

Exemplu: Parcurgerea BST-ului


const printNode = (value) => console.log(value);

bst.inOrderTraverse(printNode);   // Ieșire: 3 5 8 9 10 11 12 13 14 15 18 20 25
bst.preOrderTraverse(printNode);  // Ieșire: 11 5 3 9 8 10 15 13 12 14 20 18 25
bst.postOrderTraverse(printNode); // Ieșire: 3 8 10 9 12 14 13 18 25 20 15 11

Valori Minime și Maxime

Găsirea valorilor minime și maxime într-un BST este simplă, datorită naturii sale ordonate.


min() {
  return this.minNode(this.root);
}

minNode(node) {
  let current = node;
  while (current !== null && current.left !== null) {
    current = current.left;
  }
  return current;
}

max() {
  return this.maxNode(this.root);
}

maxNode(node) {
  let current = node;
  while (current !== null && current.right !== null) {
    current = current.right;
  }
  return current;
}

Exemplu: Găsirea valorilor minime și maxime


console.log(bst.min().key); // Ieșire: 3
console.log(bst.max().key); // Ieșire: 25

Aplicații Practice ale Arborilor Binari de Căutare

Arborii Binari de Căutare sunt utilizați într-o varietate de aplicații, inclusiv:

Considerații de Performanță

Performanța unui BST depinde de structura sa. În cel mai bun caz, un BST echilibrat permite o complexitate a timpului logaritmică pentru operațiile de inserare, căutare și ștergere. Cu toate acestea, în cel mai rău caz (de exemplu, un arbore degenerat), complexitatea timpului se poate degrada la timp liniar.

Arbori Echilibrați vs. Neechilibrați

Un BST echilibrat este unul în care înălțimea subarborilor stâng și drept ai fiecărui nod diferă cu cel mult unu. Algoritmii de auto-echilibrare, cum ar fi arborii AVL și arborii Roșu-Negru, asigură că arborele rămâne echilibrat, oferind performanțe consistente. Diferite regiuni ar putea necesita diferite niveluri de optimizare în funcție de încărcarea serverului; echilibrarea ajută la menținerea performanței sub o utilizare globală ridicată.

Complexitatea Timpului

Concepte Avansate de BST

Arbori cu Auto-Echilibrare

Arborii cu auto-echilibrare sunt BST-uri care își ajustează automat structura pentru a menține echilibrul. Acest lucru asigură că înălțimea arborelui rămâne logaritmică, oferind performanțe consistente pentru toate operațiile. Arborii comuni cu auto-echilibrare includ arborii AVL și arborii Roșu-Negru.

Arbori AVL

Arborii AVL mențin echilibrul asigurând că diferența de înălțime dintre subarborii stâng și drept ai oricărui nod este de cel mult unu. Când acest echilibru este perturbat, se efectuează rotații pentru a restabili echilibrul.

Arbori Roșu-Negru

Arborii Roșu-Negru folosesc proprietăți de culoare (roșu sau negru) pentru a menține echilibrul. Sunt mai complexi decât arborii AVL, dar oferă performanțe mai bune în anumite scenarii.

Exemplu de Cod JavaScript: Implementare Completă a unui Arbore Binar de Căutare


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  insert(key) {
    const newNode = new Node(key);

    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
  }

  insertNode(node, newNode) {
    if (newNode.key < node.key) {
      if (node.left === null) {
        node.left = newNode;
      } else {
        this.insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null) {
        node.right = newNode;
      } else {
        this.insertNode(node.right, newNode);
      }
    }
  }

  search(key) {
    return this.searchNode(this.root, key);
  }

  searchNode(node, key) {
    if (node === null) {
      return false;
    }

    if (key < node.key) {
      return this.searchNode(node.left, key);
    } else if (key > node.key) {
      return this.searchNode(node.right, key);
    } else {
      return true;
    }
  }

  remove(key) {
    this.root = this.removeNode(this.root, key);
  }

  removeNode(node, key) {
    if (node === null) {
      return null;
    }

    if (key < node.key) {
      node.left = this.removeNode(node.left, key);
      return node;
    } else if (key > node.key) {
      node.right = this.removeNode(node.right, key);
      return node;
    } else {
      // cheia este egală cu cheia nodului

      // cazul 1 - un nod frunză
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      }

      // cazul 2 - nodul are doar 1 copil
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }

      // cazul 3 - nodul are 2 copii
      const aux = this.findMinNode(node.right);
      node.key = aux.key;
      node.right = this.removeNode(node.right, aux.key);
      return node;
    }
  }

  findMinNode(node) {
    let current = node;
    while (current != null && current.left != null) {
      current = current.left;
    }
    return current;
  }

  min() {
    return this.minNode(this.root);
  }

  minNode(node) {
    let current = node;
    while (current !== null && current.left !== null) {
      current = current.left;
    }
    return current;
  }

  max() {
    return this.maxNode(this.root);
  }

  maxNode(node) {
    let current = node;
    while (current !== null && current.right !== null) {
      current = current.right;
    }
    return current;
  }

  inOrderTraverse(callback) {
    this.inOrderTraverseNode(this.root, callback);
  }

  inOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.inOrderTraverseNode(node.left, callback);
      callback(node.key);
      this.inOrderTraverseNode(node.right, callback);
    }
  }

  preOrderTraverse(callback) {
    this.preOrderTraverseNode(this.root, callback);
  }

  preOrderTraverseNode(node, callback) {
    if (node !== null) {
      callback(node.key);
      this.preOrderTraverseNode(node.left, callback);
      this.preOrderTraverseNode(node.right, callback);
    }
  }

  postOrderTraverse(callback) {
    this.postOrderTraverseNode(this.root, callback);
  }

  postOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.postOrderTraverseNode(node.left, callback);
      this.postOrderTraverseNode(node.right, callback);
      callback(node.key);
    }
  }
}

// Exemplu de Utilizare
const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

const printNode = (value) => console.log(value);

console.log("Parcurgere in-ordine:");
bst.inOrderTraverse(printNode);

console.log("Parcurgere pre-ordine:");
bst.preOrderTraverse(printNode);

console.log("Parcurgere post-ordine:");
bst.postOrderTraverse(printNode);

console.log("Valoare minimă:", bst.min().key);
console.log("Valoare maximă:", bst.max().key);

console.log("Căutare pentru 9:", bst.search(9));
console.log("Căutare pentru 2:", bst.search(2));

bst.remove(7);
console.log("Căutare pentru 7 după ștergere:", bst.search(7));

Concluzie

Arborii Binari de Căutare sunt o structură de date puternică și versatilă cu numeroase aplicații. Acest ghid a oferit o privire de ansamblu cuprinzătoare asupra BST-urilor, acoperind structura, operațiile și implementarea lor în JavaScript. Înțelegând principiile și tehnicile discutate în acest ghid, dezvoltatorii din întreaga lume pot utiliza eficient BST-urile pentru a rezolva o gamă largă de probleme în dezvoltarea de software. De la gestionarea bazelor de date globale la optimizarea algoritmilor de căutare, cunoașterea BST-urilor este un atu de neprețuit pentru orice programator.

Pe măsură ce vă continuați călătoria în informatică, explorarea conceptelor avansate precum arborii cu auto-echilibrare și diversele lor implementări vă va spori și mai mult înțelegerea și capacitățile. Continuați să practicați și să experimentați cu diferite scenarii pentru a stăpâni arta utilizării eficiente a Arborilor Binari de Căutare.